package com.microservice.skeleton.upms.config;

import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.types.ResolvedObjectType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.Sets;
import com.microservice.skeleton.upms.constraints.ConstraintReader;
import com.microservice.skeleton.upms.constraints.ConstraintReaderImpl;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import springfox.documentation.RequestHandler;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.schema.ModelReference;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.ModelBuilderPlugin;
import springfox.documentation.spi.schema.ModelPropertyBuilderPlugin;
import springfox.documentation.spi.schema.contexts.ModelContext;
import springfox.documentation.spi.schema.contexts.ModelPropertyContext;
import springfox.documentation.spi.service.ApiListingBuilderPlugin;
import springfox.documentation.spi.service.ExpandedParameterBuilderPlugin;
import springfox.documentation.spi.service.OperationBuilderPlugin;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.*;
import springfox.documentation.swagger.common.SwaggerPluginSupport;

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Strings.emptyToNull;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;

/**
 * @author: lijin
 * @date: 2018年10月28日
 */

@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER - 500)
public class JavadocExBuilderPlugin implements ApiListingBuilderPlugin, OperationBuilderPlugin, ParameterBuilderPlugin, ExpandedParameterBuilderPlugin, ModelBuilderPlugin, ModelPropertyBuilderPlugin, InitializingBean {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private JavadocJsonReader javadocJsonReader;

    private ConstraintReader constraintReader;


    @Override
    public void apply(ModelContext context) {
        /*
        ApiModel annotation = AnnotationUtils.findAnnotation(forClass(context), ApiModel.class);
        if (annotation != null) {
            List<ModelReference> modelRefs = new ArrayList<ModelReference>();
            for (Class<?> each : annotation.subTypes()) {
                modelRefs.add(modelRefFactory(context, typeNameExtractor)
                        .apply(typeResolver.resolve(each)));
            }
            context.getBuilder()
                    .description(annotation.description())
                    .discriminator(annotation.discriminator())
                    .subTypes(modelRefs);
        }
*/

        ResolvedObjectType type = (ResolvedObjectType) context.getType();
        Class<?> modelClass = type.getErasedType();
        String classComment = javadocJsonReader.resolveClassComment(modelClass);
        if (StringUtils.hasText(classComment)) context.getBuilder().description(classComment);

    }

    @Override
    public void apply(ModelPropertyContext context) {
        /*
        Optional<ApiModelProperty> annotation = Optional.absent();

        if (context.getAnnotatedElement().isPresent()) {
            annotation = annotation.or(findApiModePropertyAnnotation(context.getAnnotatedElement().get()));
        }
        if (context.getBeanPropertyDefinition().isPresent()) {
            annotation = annotation.or(findPropertyAnnotation(
                    context.getBeanPropertyDefinition().get(),
                    ApiModelProperty.class));
        }
        if (annotation.isPresent()) {
            context.getBuilder()
                    .allowableValues(annotation.transform(toAllowableValues()).orNull())
                    .required(annotation.transform(toIsRequired()).or(false))
                    .readOnly(annotation.transform(toIsReadOnly()).or(false))
                    .description(annotation.transform(toDescription(descriptions)).orNull())
                    .isHidden(annotation.transform(toHidden()).or(false))
                    .type(annotation.transform(toType(context.getResolver())).orNull())
                    .position(annotation.transform(toPosition()).or(0))
                    .example(annotation.transform(toExample()).orNull());
        }
        */

        if (context.getBeanPropertyDefinition().isPresent()) {
            BeanPropertyDefinition beanPropertyDefinition = context.getBeanPropertyDefinition().get();
            AnnotatedField field = beanPropertyDefinition.getField();
            Class<?> declaringClass = field.getDeclaringClass();
            String fieldName = field.getName();
            String description = getDescription(declaringClass, fieldName);
            if(description != null) context.getBuilder().description(description);
        }
    }

    @Override
    public void apply(OperationContext context) {
        /*
        String notes = context.requestMappingPattern() + PERIOD + context.httpMethod().toString() + ".notes";
        if (StringUtils.hasText(notes) && StringUtils.hasText(environment.getProperty(notes))) {
            context.operationBuilder().notes("<b>" + context.getName() + "</b><br/>" + environment.getProperty(notes));
        }
        String returnDescription = context.requestMappingPattern() + PERIOD + context.httpMethod().toString()
                + ".return";
        if (StringUtils.hasText(returnDescription) && StringUtils.hasText(environment.getProperty(returnDescription))) {
            context.operationBuilder().summary("returns " + environment.getProperty(returnDescription));
        }
        String throwsDescription = context.requestMappingPattern() + PERIOD + context.httpMethod().toString()
                + ".throws.";
        int i = 0;
        Set<ResponseMessage> responseMessages = new HashSet<ResponseMessage>();
        while (StringUtils.hasText(throwsDescription + i)
                && StringUtils.hasText(environment.getProperty(throwsDescription + i))) {
            String[] throwsValues = StringUtils.split(environment.getProperty(throwsDescription + i), "-");
            if (throwsValues.length == 2) {
                // TODO[MN]: proper mapping once
                // https://github.com/springfox/springfox/issues/521 is solved
                String thrownExceptionName = throwsValues[0];
                String throwComment = throwsValues[1];
                ModelReference model = new ModelRef(thrownExceptionName);
                ResponseMessage message = new ResponseMessageBuilder().code(500).message(throwComment)
                        .responseModel(model).build();
                responseMessages.add(message);
            }
            i++;
        }
        context.operationBuilder().responseMessages(responseMessages);
        */

        Class<?> declaringClass = getOperationDeclareClass(context);
        if (declaringClass == null) return;

        String methodComment = javadocJsonReader.resolveMethodComment(declaringClass, context.getName());
        if (StringUtils.hasText(methodComment)) {
            context.operationBuilder().notes(methodComment);
        }
    }

    private Class<?> getOperationDeclareClass(OperationContext context) {
        Class<? extends OperationContext> contextClass = context.getClass();
        Class<?> declaringClass = null;
        try {
            Field requestContextField = contextClass.getDeclaredField("requestContext");
            requestContextField.setAccessible(true);
            RequestMappingContext requestMappingContext = (RequestMappingContext) requestContextField.get(context);
            Class<? extends RequestMappingContext> requestMappingContextClass = requestMappingContext.getClass();
            Field handlerField = requestMappingContextClass.getDeclaredField("handler");
            handlerField.setAccessible(true);
            RequestHandler requestHandler = (RequestHandler) handlerField.get(requestMappingContext);
            declaringClass = requestHandler.declaringClass();

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return declaringClass;
    }

    @Override
    public void apply(ParameterContext parameterContext) {
        OperationContext operationContext = parameterContext.getOperationContext();
        Class<?> declareClass = getOperationDeclareClass(operationContext);
        Class<? extends OperationContext> operationContextClass = operationContext.getClass();
        ResolvedMethodParameter resolvedMethodParameter = parameterContext.resolvedMethodParameter();
        int parameterIndex = resolvedMethodParameter.getParameterIndex();
        try {
            Field requestContext = operationContextClass.getDeclaredField("requestContext");
            requestContext.setAccessible(true);
            RequestMappingContext requestMappingContext = (RequestMappingContext) requestContext.get(operationContext);
            Class<? extends RequestMappingContext> requestMappingContextClass = requestMappingContext.getClass();
            Field handler = requestMappingContextClass.getDeclaredField("handler");
            handler.setAccessible(true);
            RequestHandler requestHandler = (RequestHandler) handler.get(requestMappingContext);
            HandlerMethod handlerMethod = requestHandler.getHandlerMethod();
            MethodParameter[] methodParameters = handlerMethod.getMethodParameters();

            java.util.Optional<MethodParameter> first = Arrays.stream(methodParameters).filter(param -> param.getParameterIndex() == parameterIndex).findFirst();
            first.ifPresent(methodParameter -> {
                List<String> constraintMessages = constraintReader.getConstraintMessages(methodParameter);
                StringBuilder sb = new StringBuilder();
                for (String constraintMessage : constraintMessages) {
                    sb.append(constraintMessage).append(",");
                }

                String validationMessage = null;
                if(sb.length() > 0) validationMessage = "( " + sb.subSequence(0, sb.length()-1)+ " )";

                String methodComment = javadocJsonReader.resolveMethodParameterComment(declareClass, operationContext.getName(), resolvedMethodParameter.defaultName().get());
                String description = null;
                if (StringUtils.hasText(methodComment)) description = methodComment + (validationMessage == null?"":validationMessage);
                else description = validationMessage;
                if(description !=null) parameterContext.parameterBuilder().description(description);
            });
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return SwaggerPluginSupport.pluginDoesApply(delimiter);
    }

    @Override
    public void apply(ApiListingContext apiListingContext) {
        Optional<? extends Class<?>> controller = apiListingContext.getResourceGroup().getControllerClass();
        if (controller.isPresent()) {
            String classComment = javadocJsonReader.resolveClassComment(controller.get());
            if (StringUtils.hasText(classComment))
                apiListingContext.apiListingBuilder()
                        .description(classComment);
        }
    }

    @Override
    public void apply(ParameterExpansionContext context) {
        ResolvedType paramType = ((MyParameterExpansionContext) context).getExpansionContext().getParamType();
        Class<?> declareClass = paramType.getErasedType();
        String fieldName = context.getFieldName();
        String description = getDescription(declareClass, fieldName);
        if(description!=null) context.getParameterBuilder().description(description);

    }

    private String getDescription(Class<?> declareClass, String fieldName) {
        List<String> optionalMessages = constraintReader.getOptionalMessages(declareClass, fieldName);
        Boolean required = false;
        for (String optionalMessage : optionalMessages) {
            if("false".equals(optionalMessage)) {
                required = true;
                break;
            }
        }
        List<String> constraintMessages = constraintReader.getConstraintMessages(declareClass, fieldName);


        StringBuilder sb = new StringBuilder();
        for (String constraintMessage : constraintMessages) {
            sb.append(constraintMessage).append(",");
        }

        String validationMessage = null;
        if(sb.length() > 0) validationMessage = "\r\n" + sb.subSequence(0, sb.length()-1);

        String fieldComment = javadocJsonReader.resolveFieldComment(declareClass, fieldName);
        String description = null;
        if (StringUtils.hasText(fieldComment)) description = fieldComment + (validationMessage == null?"":validationMessage);
        else description = validationMessage;
        return description;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        constraintReader = ConstraintReaderImpl.create(objectMapper);
    }
}
